#
# Copyright 2009 Red Hat, Inc. and/or its affiliates.
#
# Licensed to you under the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.  See the files README and
# LICENSE_GPL_v2 which accompany this distribution.
#

from glob import glob
import os
import pwd
import grp
import errno

import sd
import sp
import misc
import safelease
import nfs
import storage_exception as se
import fileVolume
import image

from remoteFileHandler import remoteFileHandlerPool
ros = remoteFileHandlerPool

REMOTE_PATH = "REMOTE_PATH"

class FileSDMetadata(sd.SDMetadata):
    """
    FileSDMetadata implements metadata extractor/committer over a simple file
    """

    def __init__(self, metafile):
        sd.SDMetadata.__init__(self)
        # FileSDMetadata is kept in the file
        self.metafile = metafile
        # FileSD keeps additional parameter in its metadata
        self.metakeys.append(REMOTE_PATH)

    def _get(self):
        # Read file using /bin/dd command with O_DIRECT.
        # We cannot use open().readlines() because of python's
        # open() not support O_DIRECT
        return misc.readfile(self.metafile)

    def _set(self, metadata):
        metadata = [i + '\n' for i in metadata]
        ros.writeLines(self.metafile + ".new", metadata)
        ros.rename(self.metafile + ".new", self.metafile)

def createmetafile(path, size_str):
    try:
        size = sd.sizeStr2Int(size_str)

        file(path, "w").truncate(size)
    except Exception, e:
        raise se.StorageDomainMetadataCreationError("create meta file failed: %s: %s" % (path, str(e)))

class FileStorageDomain(sd.StorageDomain):
    def __init__(self, sdUUID):
        sd.StorageDomain.__init__(self, sdUUID)
        mountPath = os.path.join(self.storage_repository, sd.DOMAIN_MNT_POINT)
        # Using glob might look like the simplest thing to do but it isn't
        # If one of the mounts is stuck it'll cause the entire glob to fail
        # and you wouldn't be able to access any domain
        def checkPath(mnt):
            possiblePath = os.path.join(mountPath, mnt, self.sdUUID)
            try:
                if ros.lexists(possiblePath):
                    return possiblePath
            except OSError:
                return None

        domainPath = None
        for path in misc.tmap(checkPath, os.listdir(mountPath)):
            if path is not None:
                #NB: We do not support a case where there is more then one result.
                #    having two domains with the same uuid is unsupported and will
                #    cause undefined behaviour
                domainPath = path
                break

        if domainPath is None:
            self.log.error("Underlying storage for domain %s does not exist" % sdUUID)
            raise se.StorageDomainDoesNotExist(sdUUID)

        self.mountpoint = os.path.dirname(domainPath)
        self.remotePath = os.path.basename(self.mountpoint)
        self.domaindir = os.path.join(self.mountpoint, self.sdUUID)
        self.metafile = os.path.join(self.mountpoint, self.sdUUID,
                                sd.DOMAIN_META_DATA, sd.METADATA)
        if not misc.fileexists(self.metafile):
            raise se.StorageDomainMetadataNotFound(sdUUID, self.metafile)
        self._metadata = FileSDMetadata(self.metafile)
        self.imageGarbageCollector()

    @classmethod
    def create(cls, sdUUID, domainName, domClass, remotePath, storageType):
        """ Create new storage domain.
            'sdUUID' - Storage Domain UUID
            'domainName' - storage domain name ("iso" or "data domain name")
            'remotePath' - server:/export_path
            'domClass' - Data/Iso
        """
        cls.log.info("(fileSD.create) sdUUID=%s domainName=%s remotePath=%s "
            "domClass=%s", sdUUID, domainName, remotePath, domClass)
        # Create local path
        mntPath = nfs.transformPath(remotePath)

        mountpoint = os.path.join(cls.storage_repository,
            sd.DOMAIN_MNT_POINT, mntPath)
        # Make sure the underlying file system is mounted
        if not ros.ismount(mountpoint):
            raise se.StorageDomainFSNotMounted(remotePath)

        nfs.validateAccess(mountpoint)

        # Make sure there are no remnants of other domain
        mdpat = os.path.join(mountpoint, "*", sd.DOMAIN_META_DATA)
        if len(ros.glob(mdpat)) > 0:
            raise se.StorageDomainNotEmpty(remotePath)

        domaindir = os.path.join(mountpoint, sdUUID)

        # create domain metadata folder
        metadatadir = os.path.join(domaindir, sd.DOMAIN_META_DATA)
        ros.makedirs(metadatadir, 0775)

        createmetafile(os.path.join(metadatadir, sd.LEASES), sd.LEASES_SIZE)
        createmetafile(os.path.join(metadatadir, sd.IDS), sd.IDS_SIZE)
        createmetafile(os.path.join(metadatadir, sd.INBOX), sd.INBOX_SIZE)
        createmetafile(os.path.join(metadatadir, sd.OUTBOX), sd.OUTBOX_SIZE)

        metafile = os.path.join(metadatadir, sd.METADATA)

        md = FileSDMetadata(metafile)
        # initialize domain metadata content
        md.metadata = [
            "%s=%s" % (sd.VERSION, sd.SDMETADATA_VERSION),
            "%s=%s" % (sd.SDUUID, sdUUID),
            "%s=%s" % (sd.TYPE, sd.type2name(storageType)),
            "%s=%s" % (sd.CLASS, sd.class2name(domClass)),
            "%s=%s" % (sd.DESCRIPTION, domainName),
            "%s=%s" % (sd.ROLE, sd.REGULAR_DOMAIN),
            "%s=%s" % (sd.POOL, ''),
            "%s=%s" % (sd.MASTER_VER, 0),
            "%s=%s" % (sd.SPM_ID, -1),
            "%s=%s" % (sd.LVER,-1),
            "%s=%s" % (sd.LOCK_POLICY, ''),
            "%s=%s" % (sd.LOCK_RENEWAL_INTERVAL_SEC, ''),
            "%s=%s" % (sd.LEASE_TIME_SEC, ''),
            "%s=%s" % (sd.IO_OP_TIMEOUT_SEC, ''),
            "%s=%s" % (sd.LEASE_RETRIES, ''),
            "%s=%s" % (REMOTE_PATH, remotePath)
        ]

        # To be on a safe side
        md.clearcache()

        # create domain images folder
        imagesdir = os.path.join(domaindir, sd.DOMAIN_IMAGES)
        if not ros.exists(imagesdir):
            ros.makedirs(imagesdir)

        # create special imageUUID for ISO/Floppy volumes
        if domClass is sd.ISO_DOMAIN:
            isoimagedir = os.path.join(imagesdir, sd.ISO_IMAGE_UUID)
            if not ros.exists(isoimagedir):
                ros.makedirs(isoimagedir)

        fsd = FileStorageDomain(sdUUID)
        fsd.initSPMlease()

        return fsd


    def produceVolume(self, imgUUID, volUUID=None):
        """
        Produce a type specific volume object
        """
        return fileVolume.FileVolume(self.sdUUID, self.getMetaParam(sd.POOL), imgUUID, volUUID)


    def getVolumeClass(self):
        """
        Return a type specific volume generator object
        """
        return fileVolume.FileVolume


    def createVolume(self, isStopping, imgUUID, size, volFormat, preallocate, diskType, volUUID, desc, srcImgUUID, srcVolUUID):
        """
        Create a new volume
        """
        return fileVolume.FileVolume.create(isStopping, self.sdUUID, self.getMetaParam(sd.POOL), imgUUID, size, volFormat, preallocate, diskType, volUUID, desc, srcImgUUID, srcVolUUID)


    def initSPMlease(self):
        """
        Initialize the SPM lease
        """
        (rc, out, err) = safelease.initLock(os.path.join(self.domaindir,
            sd.DOMAIN_META_DATA, sd.LEASES))
        if rc != 0:
            self.log.error("fileSD.create could not initialise spm lease (%s): %s" % (rc, out))
        else:
            self.log.debug("fileSD.create - lease initialized successfully")

    def validate(self):
        """Validate that the storage domain is accessible.
        """
        self.log.info("(FileStorageDomain.validate) sdUUID=%s", self.sdUUID)
        self.Metadata(cache=False)
        return True

    def validateMaster(self):
        """Validate that the master storage domain is correct.
        """
        stat = {'mount' : True, 'valid' : True}
        if not self.isMaster():
            return stat

        masterdir = os.path.join(self.domaindir, sd.MASTER_DIR_NAME)
        pdir = os.path.join(masterdir, sd.VMS_DIR_NAME)
        if not misc.fileexists(pdir):
            stat['valid'] = False
            return stat
        pdir = os.path.join(masterdir, sd.TASKS_DIR_NAME)
        if not misc.fileexists(pdir):
            stat['valid'] = False
            return stat

        return stat

    def getAllImages(self):
        """Fetch the list of the Image UUIDs
        """
        # Get Volumes of an image
        pattern = os.path.join(self.storage_repository,
                               self.getMetaParam(sd.POOL),
                               self.sdUUID, sd.DOMAIN_IMAGES)
        pattern = os.path.join(pattern, sp.UUID_PATTERN)
        files = ros.glob(pattern)
        imgList = []
        for i in files:
            if ros.isdir(i):
                imgList.append(os.path.basename(i))
        return imgList

    def format(self):
        """Format detached storage domain.
           This removes all data from the storage domain.
        """
        self.log.info("(FileStorageDomain.format) sdUUID=%s", self.sdUUID)
        misc.cleanupdir(self.domaindir, ignoreErrors = False)
        return True

    def getRemotePath(self):
        return self.remotePath

    def getInfo(self):
        """
        Get storage domain info
        """
        ##self.log.info("(FileStorageDomain.getInfo) sdUUID=%s", self.sdUUID)
        # First call parent getInfo() - it fills in all the common details
        info = sd.StorageDomain.getInfo(self)
        # Now add fileSD specific data
        info['remotePath'] = ''
        mounts = misc.getMounts()
        for mount in mounts:
            if self.mountpoint == mount[1]:
                info['remotePath'] = mount[0]
                break

        return info

    def getStats(self):
        """Get storage domain statistics
        """
        ##self.log.info("(FileStorageDomain.getStats) sdUUID=%s", self.sdUUID)
        stats = {'disktotal':'', 'diskfree':'', 'mdavalid':True, 'mdathreshold':True}
        try:
            st = ros.statvfs(self.domaindir)
            stats['disktotal'] = str(st.f_frsize * st.f_blocks)
            stats['diskfree'] = str(st.f_frsize * st.f_bavail)
        except OSError, e:
            self.log.info("(FileStorageDomain.getStats) sdUUID=%s %s", self.sdUUID, str(e))
            if e.errno == errno.ESTALE:
                raise se.FileStorageDomainStaleNFSHandle
            raise se.StorageDomainAccessError(self.sdUUID)
        return stats

    def getIsoList(self, extension):
        """Get list of all ISO/Floppy images
            'extension' - 'iso'/'floppy' for ISO/Floppy images
        """
        self.log.debug("list iso domain %s ext %s" % (self.sdUUID, extension))
        isolist = []
        uid = pwd.getpwnam('vdsm').pw_uid
        gid = grp.getgrnam('kvm').gr_gid
        isoPat = os.path.join(self.domaindir, sd.DOMAIN_IMAGES,
                              sd.ISO_IMAGE_UUID, '*.*')
        files = ros.glob(isoPat)
        list = [ f for f in files if f[-4:].lower() == "." + extension.lower()]
        for entry in list:
            try:
                st = ros.stat(entry)
                # Check to proper uid and gid
                if st.st_uid == uid and st.st_gid == gid:
                    isolist.append(os.path.basename(entry))
            except Exception, e:
                misc.logException(e)

        return isolist

    def mountMaster(self):
        """
        Mount the master metadata file system. Should be called only by SPM.
        """
        masterdir = os.path.join(self.domaindir, sd.MASTER_DIR_NAME)
        if not ros.exists(masterdir):
            ros.mkdir(masterdir, 0755)

    def unmountMaster(self):
        """
        Unmount the master metadata file system. Should be called only by SPM.
        """
        pass


    def selftest(self):
        """
        Run internal self test
        """
        if not nfs.isMounted("", self.mountpoint):
            raise se.StorageDomainFSNotMounted

        try:
            ros.statvfs(self.domaindir)
        except OSError, e:
            if e.errno == errno.ESTALE:
                # In case it is "Stale NFS handle" we are taking preventive
                # measures and unmounting this NFS resource. Chances are
                # that is the most intelligent thing we can do in this
                # situation anyway.
                self.log.debug("Unmounting stale file system %s", self.mountpoint)
                nfs.umount("", self.mountpoint)
                raise se.FileStorageDomainStaleNFSHandle
            raise

        return True

    def imageGarbageCollector(self):
        """
        Image Garbage Collector
        remove the remnants of the removed images (they could be left sometimes
        (on NFS mostly) due to lazy file removal
        """
        removedPattern = os.path.join(self.domaindir, sd.DOMAIN_IMAGES,
            image.REMOVED_IMAGE_PREFIX+'*')
        removedImages = glob(removedPattern)
        self.log.debug("Removing remnants of deleted images %s" % removedImages)
        for imageDir in removedImages:
            misc.cleanupdir(imageDir)


def getFileStorageDomainList():
    domlist = glob(os.path.join(sd.StorageDomain.storage_repository,
                sd.DOMAIN_MNT_POINT, '*:*'))
    files = []
    def collectMetaFiles(possibleDomain):
        try:
            metaFiles = ros.glob(os.path.join(possibleDomain, sp.UUID_PATTERN, sd.DOMAIN_META_DATA))
            files.extend(metaFiles)
        except OSError:
            pass

    misc.tmap(collectMetaFiles, domlist)

    dl = []
    for f in files:
        if os.path.basename(os.path.dirname(f)) != sd.MASTER_DIR_NAME:
            dl.append(FileStorageDomain(os.path.basename(os.path.dirname(f))))

    return dl

